--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Commit 7c2655aff065a872fd79c0e2466ef1b2ed08a6bf
Parents : 65f1b2c
Author : Ivan <e46112d44649266d71fe2193e00a4710>
Signature : T66BB85Valid, signed by author
Date : 2026-07-18T15:36:13-05:00
feat: update network visualiser with camera preservation logic and update zoom handling in graphToSceneRequest
Changes
8 files changed, 63 insertions(+), 20 deletions(-)
Diff
diff --git a/meshchatx.rsm b/meshchatx.rsm
index 1a418ad4..82af6b8e 100644
Binary files a/meshchatx.rsm and b/meshchatx.rsm differ
diff --git a/meshchatx/src/frontend/js/networkVisualiserWebGLEngine.js b/meshchatx/src/frontend/js/networkVisualiserWebGLEngine.js
index 6dfadaea..4c377825 100644
--- a/meshchatx/src/frontend/js/networkVisualiserWebGLEngine.js
+++ b/meshchatx/src/frontend/js/networkVisualiserWebGLEngine.js
@@ -190,7 +190,8 @@ export function graphToSceneRequest(graphNodes, graphEdges, view) {
height: view?.height || 600,
cam_x: view?.camX ?? 0,
cam_y: view?.camY ?? 0,
- zoom: view?.zoom > 0 ? view.zoom : 1,
+ // zoom === 0 means "preserve camera" for Scene.Set. Do not coerce to 1.
+ zoom: typeof view?.zoom === "number" ? view.zoom : 1,
};
}
@@ -300,11 +301,23 @@ export function createVisualiserWebGLEngine(canvas, hooks = {}) {
}
const size = renderer.resize();
const preserveCamera = viewOpts.preserveCamera !== false && nodeCount > 0;
+ let prevCam = null;
+ if (preserveCamera) {
+ const buf = callScene("meshchatxVisualiserSceneGetDrawBuffers");
+ if (buf && buf.ok !== false) {
+ prevCam = {
+ x: buf.camX || 0,
+ y: buf.camY || 0,
+ zoom: buf.zoom > 0 ? buf.zoom : 1,
+ };
+ }
+ }
const req = graphToSceneRequest(graphNodes, graphEdges, {
width: size.width,
height: size.height,
camX: viewOpts.camX ?? 0,
camY: viewOpts.camY ?? 0,
+ // 0 = Scene.Set keeps the current pan/zoom (auto-refresh must not reset).
zoom: preserveCamera ? 0 : viewOpts.zoom > 0 ? viewOpts.zoom : 1,
});
const got = callVisualiserWasmJson("meshchatxVisualiserSceneSet", JSON.stringify(req));
@@ -313,6 +326,9 @@ export function createVisualiserWebGLEngine(canvas, hooks = {}) {
}
nodeCount = got.nodes || 0;
edgeCount = got.edges || 0;
+ if (preserveCamera && prevCam) {
+ callScene("meshchatxVisualiserSceneSetCamera", prevCam.x, prevCam.y, prevCam.zoom);
+ }
rebuildTexMeta();
dirty = true;
iconLoadGen += 1;
diff --git a/meshchatx/src/frontend/public/vendor/visualiser-wasm/integrity.json b/meshchatx/src/frontend/public/vendor/visualiser-wasm/integrity.json
index da43ac84..939ece90 100644
--- a/meshchatx/src/frontend/public/vendor/visualiser-wasm/integrity.json
+++ b/meshchatx/src/frontend/public/vendor/visualiser-wasm/integrity.json
@@ -1,6 +1,6 @@
{
"version": "1.2.0",
- "wasm": "sha384-c+lFXFxN/i2AcZZSeL7Yg72zty8ZP3YUVcirt28JfJ/+HUPNL1LyDxZg8GgKspiy",
+ "wasm": "sha384-Z1L+iI6+ud0bH8JYg54AHdwlr+/43VFTU6s91BEQQpiTjEAfGCnH2CweclVMpX5x",
"wasmExec": "sha384-PWCs+V4BDf9yY1yjkD/p+9xNEs4iEbuvq+HezAOJiY3XL5GI6VyJXMsvnjiwNbce",
"wasmExecSource": "/usr/lib/go/lib/wasm/wasm_exec.js"
}
diff --git a/tests/backend/run_comprehensive_benchmarks.py b/tests/backend/run_comprehensive_benchmarks.py
index 41f165fb..ac6b632b 100644
--- a/tests/backend/run_comprehensive_benchmarks.py
+++ b/tests/backend/run_comprehensive_benchmarks.py
@@ -378,22 +378,25 @@ class BackendBenchmarker:
def bench_identity_operations(self):
manager = IdentityManager(self.temp_dir)
- @benchmark("Create Identity", iterations=5)
- def create_id():
- return manager.create_identity(f"Bench {random.randint(0, 1000)}")
+ # Seed exactly 50 identities for the list bench. Do not call the
+ # @benchmark-wrapped create helper here: each call runs warmup +
+ # iterations and would create ~300 dirs, not 50.
+ for i in range(50):
+ manager.create_identity(f"Seed {i}")
@benchmark("List 50 Identities", iterations=10)
def list_ids():
return manager.list_identities()
- # Seed some identities
- for i in range(50):
- create_id()
+ @benchmark("Create Identity", iterations=5)
+ def create_id():
+ return manager.create_identity(f"Bench {random.randint(0, 1000)}")
- _, res = create_id()
- self.results.append(res)
+ # List first so the metric stays at 50 identities (create would add more).
_, res = list_ids()
self.results.append(res)
+ _, res = create_id()
+ self.results.append(res)
def bench_telephony_operations(self):
dao = TelephoneDAO(self.db.provider)
diff --git a/tests/frontend/networkVisualiserWebGLEngine.test.js b/tests/frontend/networkVisualiserWebGLEngine.test.js
index 6453f46c..3843af05 100644
--- a/tests/frontend/networkVisualiserWebGLEngine.test.js
+++ b/tests/frontend/networkVisualiserWebGLEngine.test.js
@@ -213,6 +213,15 @@ describe("networkVisualiserWebGLEngine", () => {
expect(req.width).toBe(640);
});
+ it("graphToSceneRequest keeps zoom 0 so Scene.Set can preserve camera", () => {
+ const req = graphToSceneRequest([], [], { width: 100, height: 100, zoom: 0, camX: 12, camY: -4 });
+ expect(req.zoom).toBe(0);
+ expect(req.cam_x).toBe(12);
+ expect(req.cam_y).toBe(-4);
+ const def = graphToSceneRequest([], [], { width: 100, height: 100 });
+ expect(def.zoom).toBe(1);
+ });
+
it("graphToSceneRequest marks disconnected interfaces and discovered peers", () => {
const req = graphToSceneRequest(
[
diff --git a/visualiser-wasm/internal/layout/force.go b/visualiser-wasm/internal/layout/force.go
index e1000bc9..9239f5cf 100644
--- a/visualiser-wasm/internal/layout/force.go
+++ b/visualiser-wasm/internal/layout/force.go
@@ -76,7 +76,12 @@ func Settle(req Request) Result {
}
gravity := req.Gravity
- if gravity == 0 {
+ // Negative gravity means "off" (live Tick). Zero means use the default
+ // centering pull for one-shot settle. Live layout must not vacuum nodes
+ // toward the origin or users cannot shape the graph.
+ if gravity < 0 {
+ gravity = 0
+ } else if gravity == 0 {
gravity = 0.01
}
repulsion := req.Repulsion
diff --git a/visualiser-wasm/internal/layout/force_test.go b/visualiser-wasm/internal/layout/force_test.go
index e404b324..cac70f93 100644
--- a/visualiser-wasm/internal/layout/force_test.go
+++ b/visualiser-wasm/internal/layout/force_test.go
@@ -3,6 +3,7 @@
package layout_test
import (
+ "math"
"testing"
"github.com/Quad4-Software/MeshChatX/visualiser-wasm/internal/layout"
@@ -28,11 +29,26 @@ func TestSettleMovesUnfixedNodes(t *testing.T) {
t.Fatalf("me should stay fixed: %#v", res.Positions["me"])
}
// Springs should push a/b outward from the tiny start distance.
- if abs(res.Positions["a"].X) < 20 {
+ if math.Abs(res.Positions["a"].X) < 20 {
t.Fatalf("expected a to move outward, got %#v", res.Positions["a"])
}
}
+func TestSettleNegativeGravityDisablesOriginPull(t *testing.T) {
+ // Far from origin with no edges: default gravity would suck the node home.
+ res := layout.Settle(layout.Request{
+ Nodes: []layout.Node{
+ {ID: "lonely", X: 800, Y: 600, Mass: 1},
+ },
+ Iterations: 40,
+ Gravity: -1,
+ })
+ p := res.Positions["lonely"]
+ if math.Hypot(p.X-800, p.Y-600) > 5 {
+ t.Fatalf("negative gravity must not pull toward origin: start=(800,600) got=%#v", p)
+ }
+}
+
func TestSettlePreservesAndUpdatesVelocity(t *testing.T) {
nodes := []layout.Node{
{ID: "me", X: 0, Y: 0, Mass: 4, Fixed: true},
@@ -84,10 +100,3 @@ func itoa(n int) string {
}
return string(buf[i:])
}
-
-func abs(v float64) float64 {
- if v < 0 {
- return -v
- }
- return v
-}
diff --git a/visualiser-wasm/internal/scene/scene.go b/visualiser-wasm/internal/scene/scene.go
index cb3976d6..e370b3c2 100644
--- a/visualiser-wasm/internal/scene/scene.go
+++ b/visualiser-wasm/internal/scene/scene.go
@@ -271,11 +271,12 @@ func (s *Scene) Tick(steps int) {
})
}
// Softer than one-shot Settle defaults so live layout does not twitch.
+ // Gravity -1 disables origin pull so dragged layouts stay put.
_ = layout.Settle(layout.Request{
Nodes: layoutNodes,
Edges: layoutEdges,
Iterations: steps,
- Gravity: 0.004,
+ Gravity: -1,
Repulsion: 550,
SpringK: 0.018,
Damping: 0.52,
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────